In Scala (both 2 and 3), the copy
method of case classes is synthetic, and so cannot be generalized using simple type clauses. In Scala 2, this led to generically processing case classes being done via shapeless. In Scala 3, however, support for abstracting over product types is substantially improved, alongside with other new language features, which leads to the following question:
In Scala 3, how can one create a generic copy
function for all cases classes, while minimizing implementation complexity, usage complexity, and runtime resource cost?
The following, in particular, should be avoided:
- explicit, instantiated "witness" objects for type-recursive processing, like in this example;
- any need to explicitly define generic types for the "genericy copy" function call: ideally, all generics and givens should be infer-able;
- use of any external libraries, unless they fulfill the criteria defined here.
What has been tried:
Let's suppose we have the following two test case classes:
case class CC1(a: String)
case class CC2(a: String, b: Int)
A naive first attempt would be this:
def genCopyV1[CC <: Product](
cc: CC,
copy: ccMirror.MirroredElemTypes => ccMirror.MirroredElemTypes
)(using
ccMirror: ProductOf[CC]
): CC =
ccMirror.fromProduct(copy(Tuple.fromProduct(cc).asInstanceOf[ccMirror.MirroredElemTypes]))
This fails with two Not found: ccMirror
errors, since Scala 3 only supports dependent types in return values.
We can maybe try to improve it with some path-dependent types:
def genCopyV2[CC <: Product](
cc: CC,
copy: ProductOf[CC]#MirroredElemTypes => ProductOf[CC]#MirroredElemTypes
)(using
ccMirror: ProductOf[CC]
): CC =
ccMirror.fromProduct(copy(Tuple.fromProduct(cc).asInstanceOf[ccMirror.MirroredElemTypes]))
This actually compiles, but only until trying out actual usage:
println(generic.genCopyV2(CC1("a"), _ => Tuple1("b")))
//Found: Tuple1[String]
//Required: deriving.Mirror.ProductOf[CC1]#MirroredElemTypes
println(generic.genCopyV2(CC2("a", 1), (a, b) => (a + "b", b + 1)))
// Wrong number of parameters, expected: 1
the errors are likely caused by the ProductOf[CC]#MirroredElemTypes
not actually being bound to the particular Mirror
instance resolved for ProductOf[CC]
.
Finally, if we bind the tuple representation type*
:
def genCopyV3[CC <: Product, TupleType <: Tuple](cc: CC, copy: TupleType => TupleType)(using
ccMirror: ProductOf[CC],
tupleEqEnv: TupleType =:= ccMirror.MirroredElemTypes
): CC =
ccMirror.fromProduct(copy(Tuple.fromProduct(cc).asInstanceOf[TupleType]))
we can finally use the function:
println(generic.genCopyV3[CC1, String *: EmptyTuple](CC1("a"), _ => Tuple1("b")))
// CC1(b)
println(generic.genCopyV3[CC2, (String, Int)](CC2("a", 1), (a, b) => (a + "b", b + 1)))
// CC2(ab,2)
println(generic.genCopyV3(CC2("a", 1), (a: String, b: Int) => (a + "b", b + 1)))
// CC2(ab,2)
Of course, this is not very useful, since the calling code needs to either explicitly specify the generic parameters, or bind them some other way (like in the argument function signature of the final usage example).
*
for simplicity of use (not needing Tuple1
), we can also add the 1-element case class special case:
def genCopyV3[CC <: Product, Single](cc: CC, copy: Single => Single)(using
ccMirror: ProductOf[CC],
tupleEqEnv: Single *: EmptyTuple =:= ccMirror.MirroredElemTypes,
singleNotTupleEnv: NotGiven[Single =:= Tuple]
): CC =
ccMirror.fromProduct(Tuple(copy(cc.productElement(0).asInstanceOf[Single])))
Copyright Notice:Content Author:「mikołak」,Reproduced under the CC 4.0 BY-SA copyright license with a link to the original source and this disclaimer.
Link to original article:https://stackoverflow.com/questions/71812481/minimal-generic-case-class-copy-function-in-scala-3